(define selfmenuroot "<Image>/Help")     ; Change these to suit yourself         
(define selfmenuentry "Test For Duplicate Scripts v1.8...")

(define selfauthor "Kevin Payne")
(define selfcopyright "Copyright (C) 2012 Kevin Payne paynekj@hotmail.com")

; Support for this script can be found on GIMPChat: http://gimpchat.com/viewtopic.php?f=9&t=3057
;
; Version 1.0 09.11.2011 First version
; Version 1.1 10.11.2011 Add option to search all script directories
; Version 1.2 11.11.2011 Change checking options to include checking a separate directory against the installed scripts
; Version 1.3 14.11.2011 Add options to create a log file
; Version 1.4 14.11.2011 Tidying
; Version 1.5 15.11.2011 Re-write to get full menu paths into log file
; Version 1.6 24.11.2011 Direct messages to a user specified choice and add too much logging
; Version 1.7 10.05.2012 Add catch around (read stream) and around definition of searchpath-separator
; Version 1.8 25.05.2012 Make searchpath-separator work on both 2.6 and 2.8

(define selffileversion "25.05.2012 Version 1.8 - Works with 2.6/2.8")

; ##################################################################################################
; IMPORTANT if you change the file name, change this to match
(define selffilename "\n- kp24_test_for_duplicate_scripts.scm")
; I do this to get the file name into the description shown by the procedure browser
; ##################################################################################################

; What gets displayed when the mouse is hovered over the menu entry
(define selftipstrip (string-append "Test for duplicate scripts" "\n\n\nRegisters in menu " selfmenuroot  "/" selfmenuentry "\n\nScript File name - " selffilename))

; ####################### USEAGE #############################################################################################
;
;Check What?:
; All installed scripts:
;   will check the are no duplicates within all the installed scripts.
;   i.e all the scripts in the directories defined in Edit >> Preferences >> Folders >> Scripts
;
; Single directory within itself:
;   will check that all the scripts in the directory you specify, don't have any duplicates among themselves.
;    For example you could download some new scripts to /home/new-scripts and then check that there isn't any
;    duplication within that collection of scripts.
;
; Single directory against installed scripts:
;   will check that the scripts in the specified directory don't duplicate any of the installed scripts.
;    To continue with the exaple above, you could now check the the new scripts in /home/new-scripts aren't gong to
;    duplicate any of the scripts you already have installed.
;
;Logging:  (It is adviable to have the Error Console open when running this script)
; None:
;   the logging file is not used and all duplication messages are sent to the error console (or dialog boxes)
;
; Duplicates Only:
;   The duplication messages are also written to the log file
;
; Everything:
;   The duplication messages are written to the log file, along with details of scripts checked, in a pipe "|" delimited string
;
; Too Much:
;   Logs loads of useless information only of use if trying to debug a problem - unmatched paretheses for instance
;
;
;   Example of everyting log:
;
; <Image>/Filters/Alpha to Logo/3D Outline... | C:\Program Files\GIMP-2.0\share\gimp\2.0\scripts\3d-outline.scm | script-fu-3d-outline-logo-alpha | Hrvoje Horvat | 07 April, 1998
;
; Where the fields are:
; Full Menu Path | Source File Path | Procedure Name | Author String | Version String
;
;Mesages to Error Console?:
;   If selected this will direct all the messages to the error console, which will be opened if it isn't already open or docked.
;   If de-selected then the messages will be collected until the script is finished, then displayed in a single pop-up dialog.
;
; ####################################################################################################################
(define selfusage "'All installed scripts'\nwill check the are no duplicates within all the installed scripts\n\n'Single directory within itself'\nwill check all the scripts in the directory you specify don't have any duplicates among themselves.\n\n'Single directory against installed scripts'\n will check that the scripts in the specified directory don't duplicate any of the installed scripts.")

; As the constant is now defined (2.8 onwards) we need to
; put this definintion for 2.6 inside a catch statement to prevent
; it causing an error.
; and thanks to saulgoode for the tidier version
(define (kp24_searchpath_separator)
  (let* ((The_Thing (if (string=? DIR-SEPARATOR "\\") ";" ":")))
        (catch () (set! The_Thing SEARCHPATH-SEPARATOR))
        The_Thing
  )           
)

; return a list of the currently defined script directories
(define (kp24_installedScriptDirectories)
  (let* (
          (source_directory_list "")
        )
        (catch ()   ; just in case, but it shouldn't error 
          (set! source_directory_list (car (gimp-gimprc-query "script-fu-path")))  ; string containing the users script directories
        )
        (if (> (string-length source_directory_list) 0) ; just in case it did error
          (set! source_directory_list (strbreakup source_directory_list (kp24_searchpath_separator)))  ; turn the string into a list
          (set! source_directory_list (list (string-append gimp-directory DIR-SEPARATOR "scripts"))) ; no user defined script directories so make a single item list of the default directory
        )
        source_directory_list    
  )
)

; build a list of the script files in the supplied directories
(define (kp24_buildScriptFileListNew source_directory_list)  ; expect the directory list to be a list
  (let* (
           (varFileList '())
        )
        (for-each                       ; scan through the directories building a list of script files
          (lambda (directory_path)
            (set! varFileList (append varFileList (cadr (file-glob (string-append directory_path DIR-SEPARATOR "*.scm") 1) ) ) ); Get the list of script files 
          ) source_directory_list
        )
        varFileList      ; return the list
  )
)

; useful utility copied from http://www.scheme.com/tspl3/start.html#./start:s145
(define list-copy
  (lambda (ls)
    (if (null? ls)
        '()
        (cons (car ls) (list-copy (cdr ls)))
    )
  )
)

; It could be a symbol, but return a string
(define (kp24_ensure_string thing)
  (if (symbol? thing)
    (symbol->string thing)
    thing
  )
)

; utility to extract the thing that's being defined
(define (other_define_thing token)
  (let* ( (token_car "") )
        (set! token_car (car (cdr token)))
        (cond
          ((list? token_car) (car token_car))
          ((symbol? token_car) token_car)
          ((string? token_car) token_car)
          (else "too complicated!")
        )
  )
)

; build these here just to keep the line lengths down later on
(define (duplicate_report proc filepath menu author version)
  (string-append proc " in \n" filepath "\n Menu: " menu "\n Author: " author "\n Version: " version "\n")
)
(define (duplicate_report2 proc_name_list filepath_list menu_path_list author_entry_list version_entry_list list_ref)
  (duplicate_report (list-ref proc_name_list list_ref) (list-ref filepath_list list_ref) (list-ref menu_path_list list_ref) (list-ref author_entry_list list_ref) (list-ref version_entry_list list_ref))
)
(define (kp24_normal_log_entry proc filepath menu author version list_ref)
  (string-append (list-ref menu list_ref) " | " (list-ref filepath list_ref) " | " (list-ref proc list_ref) " | " (list-ref author list_ref) " | " (list-ref version list_ref))
)

; strip the underscore from the menu entry text
(define (kp24_strip_underscore string)
  (unbreakupstr (strbreakup string "_") "")
)

; Extract the Menu path from a script-fu-menu-register and append it to the appropriate menu text
; THIS WILL GO WRONG IF THERE ARE DUPLICATE PROCEDURE NAMES - IT MIGHT APPPEND THE PATH TO THE WRONG MENU TEXT !
; Note: because we get passed pointers, we can change the values in the lists
(define (kp24_add_menu_path token proc_name_list menu_entry_list menu_path_list)
  (let* ( (proc_name "") (menu_entry "") (list_ref 0) (found_remainder '())
        )
        (begin
          (set! proc_name (kp24_ensure_string (car (cdr token))))
          ; We search the list backwards, in the hope that the last entry is the correct one!
          (set! found_remainder (member proc_name (reverse proc_name_list))) ; is this procedure already in the list?
          (if (list? found_remainder)
            (begin                                                                   ; Yes, this procedure is already installed
              (set! list_ref (- (length proc_name_list) (length found_remainder)))   ; find the list index of the installed occurence
              (set! list_ref (- (length found_remainder) 1 ))
              (set! menu_entry (string-append (kp24_ensure_string (car (cdr (cdr token)))) "/" (list-ref menu_entry_list list_ref)))
              (set-car! (list-tail menu_path_list list_ref) menu_entry)
            )
            (gimp-message (string-append proc_name " somehow wasn't found !" ))
          )
        )
  )
)

; ##################################################################################################
; This is where all the hard work gets done
; ##################################################################################################
(define (kp24_checkFilesWithinDirectories varFileList filesToCheckList same_dir logging_mode log_file_name live_messages)
  (let* ( 
           (found 0) (found_remainder '()) (list_ref 0) (register_count 0) (file_count 0) (number_of_files 0)
           (token "") (token_car "") (token_thing "")
           (stream 0) (log_stream 0)
           (duplicate_count 0)
           
           (proc_name "")     (proc_name_list '())     (check_proc_name '())
           (menu_entry "")    (menu_entry_list '())    (check_menu_entry '())
                              (filepath_list '())      (check_filepath '())
                              (menu_path_list '())     (check_menu_path '())
           (author_entry "")  (author_entry_list '())  (check_author_entry '())
           (version_entry "") (version_entry_list '()) (check_version_entry '())
           
           (logging_file_path (string-append gimp-directory DIR-SEPARATOR ".." DIR-SEPARATOR log_file_name))

           (script_count 0) (number_of_scripts 0)
           
           (gimp_message_store "")
           (gimp_message_handler (car (gimp-message-get-handler))) ; remember how the user is handling error messages before we start
        )

        (if (> logging_mode 0) (set! log_stream (open-output-file logging_file_path)) )

        (if (= live_messages TRUE)  ; Set the error message handler to what the user has chosen for this script
          (gimp-message-set-handler ERROR-CONSOLE)
        )

        ; This section builds a list of scripts to check against
        (set! number_of_files (length varFileList))    ; how many files are we checking?
        (gimp-progress-set-text "Building details of installed files...")
 
        (if (> logging_mode 2) (display "These are the script files I'm checking against...\n" log_stream))
        (for-each                      ; step through each of the script files to check against
          (lambda (filepath)
            (if (> logging_mode 2) (display (string-append " " filepath "\n") log_stream))
            (set! found 0)                            ; flag to indicate if we've found what what we're looking for
            (set! stream (open-input-file filepath))  ; open the script file
            (set! file_count (+ file_count 1))
            (gimp-progress-update (/ file_count number_of_files))           ; keep the user informed
            (gimp-progress-set-text (string-append "Processing " filepath))
            (while (= found 0)                        ; loop round until we've found what what we're looking for
              (begin
                (set! token (catch 'eof (read stream)))            ; read a section from the script file
                (if (list? token)                     ; if we've got something useful
                 (begin
                  (set! token_car (symbol->string (car token)))
                  (if (> logging_mode 2) (display (string-append "   " token_car " ") log_stream))
                  (if (string=? token_car "script-fu-register")  ; check if this section is what we want
                    (begin
                      (set! proc_name (kp24_ensure_string (car (cdr token))))                ; extract the procedure name
                      (if (> logging_mode 2) (display (string-append "  " proc_name "\n") log_stream))
                      (set! menu_entry (kp24_strip_underscore (kp24_ensure_string (car (cdr (cdr token))))))         ; extract the menu entry
                      (set! author_entry (kp24_ensure_string (car (cdr (cdr (cdr (cdr (cdr token))))))))  ; extract the author entry
                      (set! version_entry (kp24_ensure_string (car (cdr (cdr (cdr (cdr (cdr (cdr token)))))))))  ; extract the version entry                         

                      (set! proc_name_list (append proc_name_list (list proc_name))) ; New name so add to list
                      (set! filepath_list (append filepath_list (list filepath)))    ; remember which file it was in
                      (set! menu_entry_list (append menu_entry_list (list menu_entry))) ; New so add to list
                      (set! author_entry_list (append author_entry_list (list author_entry))) ; New so add to list
                      (set! version_entry_list (append version_entry_list (list version_entry))) ; New so add to list
                      (set! menu_path_list (append menu_path_list (list menu_entry)))  ; add a placeholder to the list
                    )
                    ; this next bit tries to capture the menu path from the script-fu-menu-register if it exists and
                    ; add it to the front of the menu entry.
                    ; For this to work script-fu-register MUST come before script-fu-menu-register
                    (if (string=? token_car "script-fu-menu-register")  ; check if this section is what we want
                      (begin
                        (if (> logging_mode 2) (display (string-append " " proc_name "\n") log_stream))
                        (kp24_add_menu_path token proc_name_list menu_entry_list menu_path_list)
                      )
                      ; if not either script-fu-registers, then log the thing being defined if required
                      (if (> logging_mode 2) (display (string-append " " (kp24_ensure_string (other_define_thing token)) "\n") log_stream))
                    )
                  )
                 )
                 (set! found 1) ; We ran out of file
                )
              )
            )
            (if (> logging_mode 2) (display (string-append "Finished with " filepath "\n") log_stream))
            (close-input-port stream)  ; close the script file
          )
          varFileList
        )
            
        (if (> logging_mode 1) (display (string-append "Found " (number->string file_count) " script files to check against\n\n") log_stream))
        (if (= live_messages TRUE)
          (gimp-message (string-append "Found " (number->string file_count) " script files to check against"))
          (set! gimp_message_store (string-append gimp_message_store "Found " (number->string file_count) " script files to check against\n"))
        )
        (if (= same_dir TRUE)  ; If we are checking within the same directory(ies), then we've already
          (begin               ; made the list of scripts to check
             (if (> logging_mode 2) (display "Checking the same script files against themselves...\n" log_stream))
             (set! check_proc_name (list-copy proc_name_list))
             (set! check_filepath  (list-copy filepath_list))
             (set! check_menu_entry (list-copy menu_entry_list))
             (set! check_menu_path (list-copy menu_path_list))
             (set! check_author_entry (list-copy author_entry_list))
             (set! check_version_entry (list-copy version_entry_list))
          )
          (begin
            (set! number_of_files (length filesToCheckList))    ; how many files are we checking?
            (set! file_count 0)
            (if (> logging_mode 2) (display "These are the script files I'm now checking...\n" log_stream))
            (for-each                      ; step through each of the script files to check against
              (lambda (filepath)
                (if (> logging_mode 2) (display (string-append " " filepath "\n") log_stream))
                (set! found 0)                            ; flag to indicate if we've found what what we're looking for
                (set! stream (open-input-file filepath))  ; open the script file
                (set! file_count (+ file_count 1))
                (gimp-progress-update (/ file_count number_of_files))           ; keep the user informed
                (gimp-progress-set-text (string-append "Processing " filepath))
                (while (= found 0)                        ; loop round until we've found what what we're looking for
                  (begin
                    (set! token (catch 'eof (read stream)))            ; read a section from the script file
                    (if (list? token)                     ; if we've got something useful
                     (begin
                      (set! token_car (symbol->string (car token)))
                      (if (> logging_mode 2) (display (string-append "   " token_car " ") log_stream))
                      (if (string=? token_car "script-fu-register")  ; check if this section is what we want
                        (begin
                          (set! proc_name (kp24_ensure_string (car (cdr token))))                ; extract the procedure name
                          (if (> logging_mode 2) (display (string-append "  " proc_name "\n") log_stream))
                          (set! menu_entry (kp24_strip_underscore (kp24_ensure_string (car (cdr (cdr token))))))         ; extract the menu entry
                          (set! author_entry (kp24_ensure_string (car (cdr (cdr (cdr (cdr (cdr token))))))))  ; extract the author entry
                          (set! version_entry (kp24_ensure_string (car (cdr (cdr (cdr (cdr (cdr (cdr token)))))))))  ; extract the version entry                         

                          (set! check_proc_name (append check_proc_name (list proc_name))) ; New name so add to list
                          (set! check_filepath (append check_filepath (list filepath)))    ; remember which file it was in
                          (set! check_menu_entry (append check_menu_entry (list menu_entry))) ; New so add to list
                          (set! check_author_entry (append check_author_entry (list author_entry))) ; New so add to list
                          (set! check_version_entry (append check_version_entry (list version_entry))) ; New so add to list
                          (set! check_menu_path (append check_menu_path (list menu_entry)))  ; add an empty placeholder to the list
                        )
                        ; this next bit tries to capture the menu path from the script-fu-menu-register if it exists and
                        ; add it to the front of the menu entry.
                        ; For this to work script-fu-register MUST come before script-fu-menu-register
                        (if (string=? token_car "script-fu-menu-register")  ; check if this section is what we want
                          (begin
                            (if (> logging_mode 2) (display (string-append " " proc_name "\n") log_stream))
                            (kp24_add_menu_path token check_proc_name check_menu_entry check_menu_path)
                          )
                          ; if not either script-fu-registers, then log the thing being defined if required
                          (if (> logging_mode 2) (display (string-append " " (kp24_ensure_string (other_define_thing token)) "\n") log_stream))
                        )
                      )
                    )
                    (set! found 1) ; We ran out of file
                   )
                  )
                )
                (if (> logging_mode 2) (display (string-append "Finished with " filepath "\n") log_stream))
                (close-input-port stream)  ; close the script file
              )
              filesToCheckList
            )
          )
        )
 
        (set! number_of_scripts (length check_proc_name))         
        (if (= live_messages TRUE)
          (gimp-message (string-append "There are " (number->string number_of_scripts) " scripts to check"))
          (set! gimp_message_store (string-append gimp_message_store "There are " (number->string number_of_scripts) " scripts to check\n"))
        )

        ; Now we're going to run through the list of scripts to check, comparing them to the list to check against
        (set! duplicate_count 0)
        (set! script_count 0)
        (set! found 0)
        (for-each
          (lambda (proc_name)                                                   ; loop through the scripts to check
            (gimp-progress-update (/ script_count number_of_scripts))           ; keep the user informed
            (gimp-progress-set-text (string-append "Processing " proc_name))
            
            ; log the current procedure if we are logging everything
            (if (> logging_mode 1) (display (string-append (kp24_normal_log_entry check_proc_name check_filepath check_menu_path check_author_entry check_version_entry script_count) "\r\n") log_stream ))
              
            (if (= same_dir TRUE) 
              (set! found_remainder (member proc_name (list-tail proc_name_list (+ script_count 1))))  ; same directory so only need to search what's left
              (set! found_remainder (member proc_name proc_name_list))                                 ; different directories so need to search the full list
            )
            (if (list? found_remainder)     ; The procedure name was found somewhere else so we have a duplicate
              (begin
                (set! duplicate_count (+ duplicate_count 1))
                (set! list_ref  (- (length proc_name_list) (length found_remainder)) )   ; find the list index of the duplicate
                (if (= live_messages TRUE)
                  (gimp-message (string-append "duplicate: " proc_name " in \n" (list-ref check_filepath script_count)  "\nand\n" (list-ref filepath_list list_ref)))
                  (set! gimp_message_store (string-append gimp_message_store "\nduplicate: " proc_name " in \n" (list-ref check_filepath script_count)  "\nand\n" (list-ref filepath_list list_ref) "\n"))
                )
                (if (> logging_mode 0)
                  (display (string-append "\nduplicate:\n" (duplicate_report2 check_proc_name check_filepath check_menu_path check_author_entry check_version_entry script_count) (duplicate_report2 proc_name_list filepath_list menu_entry_list author_entry_list version_entry_list list_ref) "\n") log_stream)
                )
              )
            )
            (set! script_count (+ script_count 1))
          )
          check_proc_name
        )

        ; checking done, now tell the user what we found
        (if (> logging_mode 0)
          (begin
            (display (string-append "\nFinished. Found " (number->string duplicate_count) " duplicates in " (number->string number_of_scripts) " scripts") log_stream)
            (close-output-port log_stream)
            (if (= live_messages TRUE)
              (gimp-message (string-append "Log file: " logging_file_path))
              (set! gimp_message_store (string-append gimp_message_store "\nLog file: " logging_file_path "\n"))
            )
          )
        )
        (if (= live_messages TRUE)
          (gimp-message (string-append "Finished. Found " (number->string duplicate_count) " duplicates in " (number->string number_of_scripts) " scripts"))
          (set! gimp_message_store (string-append gimp_message_store "\nFinished. Found " (number->string duplicate_count) " duplicates in " (number->string number_of_scripts) " scripts"))
        )

        (if (= live_messages FALSE)
          (begin
            (gimp-message-set-handler MESSAGE-BOX)
            (gimp-message gimp_message_store)
          )
        )
        (gimp-message-set-handler gimp_message_handler)  ; Set the message handling back to how the user had it
  )
)

; ##################################################################################################
; This is the main routine that gets called from the user interface
; ##################################################################################################
(define (script-fu-kp24_test_for_duplicate_scripts Unused which_check_option explicit_directory logging_mode log_file live_messages)
  (let* ( 
           (varFileList '()) (filesToCheckList '()) (same_dir TRUE)
        )

        (gimp-progress-init "Building list of files" -1)
        (cond 
           ((= which_check_option 0) ; Check all installed
             (begin
                (set! varFileList (kp24_buildScriptFileListNew (kp24_installedScriptDirectories)))
                (set! filesToCheckList (list-copy varFileList)) 
                (set! same_dir TRUE)
             )  
           )
           ((= which_check_option 1) ; Compare single directory with itself
             (begin
                (set! varFileList (kp24_buildScriptFileListNew (list explicit_directory)))
                (set! filesToCheckList (list-copy varFileList))               
                (set! same_dir TRUE)
             )
           )
           ((= which_check_option 2) ; Compare single directory with installed
             (begin
                (set! varFileList (kp24_buildScriptFileListNew (kp24_installedScriptDirectories)))
                (set! filesToCheckList (kp24_buildScriptFileListNew (list explicit_directory)))
                (set! same_dir FALSE)
             )
           )
           ((else) (error "Unknown option for which_check_option"))
        )

        (kp24_checkFilesWithinDirectories varFileList filesToCheckList same_dir logging_mode log_file live_messages) ; go and do all the hard work
  )
)

(script-fu-register "script-fu-kp24_test_for_duplicate_scripts"
                    selfmenuentry    ; I prefer to have all of these defined at the top of the file
                    selftipstrip     ; where they are easier to find - especially for someone else
                    selfcopyright    ; who would like to change the Menu text and location.                   
                    selfauthor       ; Ironically that makes the log output of this file less useful as it just gives the symbol names!
                    selffileversion
                    ""               ; Don't need to specify image type as this script doesn't do anything to images.
                    SF-TEXT      _"Usage" selfusage
                    SF-OPTION    _"Check What?" '(_"All installed scripts" _"Single directory within itself" _"Single directory against installed scripts")
                    SF-DIRNAME   _"Single Directory" (string-append gimp-directory DIR-SEPARATOR "scripts")
                    SF-OPTION    _"Logging" '(_"None" _"Duplicates only" _"Everything" _"Too Much")
                    SF-STRING    _"Log File Name" "duplicate_scripts.txt"
                    SF-TOGGLE    _"Mesages to Error Console?" TRUE
                    )

(script-fu-menu-register "script-fu-kp24_test_for_duplicate_scripts" selfmenuroot)
